package Algorithms;

import MainProgram.Main;
import NeuronNetworkLibrary.Network;
import java.util.ArrayList;

/**
 * Pruning Algorithm.
 *
 * @author Zbyszko
 */
public class Pruning {

    private Network network;
    private int cycles; // nbr of cycles
    private boolean isDone; // flag which indicate if the processed is finished
    private double error; //current error
    private double desiredOutput[][]; // desired output
    private int hiddenNeuronCounter; //nbr od gidden neurons
    private double markErrorRate; //used to determine if the traiing is still effective
    private double maxError; //the maximum acceptance error.
    private int sinceMark; // used with markErrorRate; number of cycles sinse the rror was last marked.
    private double trainingSet[][]; //the training set
    ArrayList<MinErrorContainer> minErrorContainer;
    /**
     * Constructor for Incremental Pruning.
     *
     * @param input Training set.
     * @param output Desired output.
     * @param maxError Max. value of acceptable error.
     */
    public Pruning(double[][] input, double[][] output, double maxError) {
        this.trainingSet = input;
        this.desiredOutput = output;
        this.maxError = maxError;
    }

    /**
     * Constructor for Selective Pruning.
     *
     * @param network
     * @param input
     * @param output
     * @param maxError
     */
    public Pruning(Network network, double maxError) {
        this.maxError = maxError;
        this.network = network;
        this.minErrorContainer = new ArrayList<>();
    }

    /**
     * Initialization of basic parameters for prunning algorithm.
     */
    public void startIncremental() {
        this.hiddenNeuronCounter = 1;
        this.cycles = 0;
        this.isDone = false;

        this.network = new Network(this.trainingSet, this.desiredOutput,
                new int[]{this.hiddenNeuronCounter}, new String[]{"sigmo"},
                "linear");
    }

    /**
     * Main method responsible for incremental prunning.
     */
    public void pruneIncremental() {

        if (this.isDone) {
            return;
        }
        int order[] = Main.randomizeOrder(network.getNumberOfPatterns());

        for (int i = 0; i < network.getNumberOfPatterns(); i++) {

            this.feedforward(order[i]);
            BackPropagation.calculateBackPropagation(this.network);
            this.feedforward(order[i]);
            this.error = network.calculateRMSE();
            this.cycles += 1;

            this.increment(order[i]);
        }
    }

    /**
     * Determines if the number of hidden neurons should be increased or not. It
     * keeps track of training process for the neural network.
     *
     * @param patternNbr Identification number of training set.
     */
    protected void increment(int patternNbr) {

        boolean doit = false;

        if (this.markErrorRate == 0) {
            this.markErrorRate = this.error;
            this.sinceMark = 0;
        } else {
            this.sinceMark++;
            if (this.sinceMark > 10000) {

                if ((this.markErrorRate - this.error) < 0.01) {
                    doit = true;
                }
                this.markErrorRate = this.error;
                this.sinceMark = 0;
            }
        }

        if (this.error < this.maxError) {
            this.isDone = true;
        }

        if (doit) {
            this.cycles = 0;
            this.hiddenNeuronCounter++;

            Network.INIT_WEIGHTS_FLAG = true;
            this.network = new Network(this.trainingSet, this.desiredOutput,
                    new int[]{this.hiddenNeuronCounter}, new String[]{"sigmo"},
                    "linear");


            this.feedforward(patternNbr);
            BackPropagation.calculateBackPropagation(network);
            this.feedforward(patternNbr);
        }

    }

    public int pruneSelective() {

        final int i = this.getHiddenCounter();
        //for (int k = 0; k < this.network.getNumberOfPatterns(); k++) {

            while (findNeuron(0)) {
                ;
            }
            //System.out.println("break");
            //break;
        //}
        return (i - this.getHiddenCounter());
    }

    private int getHiddenCounter() {
        int counter = 0;
        for (int i = 0; i < network.getHiddenLayers().size(); i++) {
            for (int j = 0; j < network.getHiddenLayers().get(i).size(); j++) {
                counter++;
            }
        }

        return counter;
    }

    protected boolean findNeuron(int patternNbr) {
        
        int hiddenLength = this.network.getHiddenLayers().size()-1;
        int hiddenSize = this.network.getHiddenLayers().get(0).size()-1; //HARD-CODED!
        //int i = j = 0;
        
        while(hiddenLength >= 0) {
            while(hiddenSize >= 0 ) {

                Network trial = this.hideNeuron(hiddenLength, hiddenSize);
                double e2 = determineError(trial, patternNbr);
                minErrorContainer.add(new MinErrorContainer(
                        e2, this.getHiddenCounter()));
                //System.out.println("->" + e2);
                if (e2 < this.maxError) {
                    //System.out.println("true");
                    this.network = trial;
                    return true;
                }
                hiddenSize--;
            }
            hiddenLength--;
        }
        return false;
    }
    
    private double determineError(Network trial, int patternNbr) {
        for (int i = 0; i < trial.getNumberOfPatterns(); i++) {
            trial.calculateNetwork(i);
            
        }
        //trial.calculateNetwork(patternNbr);
        //return trial.calculateRMSE();
        return trial.getMSE();
    }

    protected Network hideNeuron(int i, int j) {
        this.network.getHiddenLayers().get(i).remove(j);
        return this.network;
    }

    /**
     * FeedForward progagation signal.
     *
     * @param i Identification number of training set.
     */
    private void feedforward(int i) {
        network.calculateNetwork(i);
    }

    public boolean isDone() {
        return isDone;
    }

    public int getCycles() {
        return cycles;
    }

    public double getError() {
        return error;
    }

    public int getHiddenNeuronCounter() {
        return hiddenNeuronCounter;
    }

    public Network getNetwork() {
        return network;
    }
    
    public class MinErrorContainer {
        private double error;
        private int neuronsNbr;

        public int getNeuronsNbr() {
            return neuronsNbr;
        }
        
        public double getError(){
            return error;
        }

        public MinErrorContainer(double error, int neuronsNbr) {
            this.error = error;
            this.neuronsNbr = neuronsNbr;
        }
    }
    
    public int getMinNumber(){
        double minError = Double.MAX_VALUE;
        int minKey = 0;
        
        for (int i = 0; i < minErrorContainer.size(); i++) {
            if(minErrorContainer.get(i).getError() < minError) {
                minError = minErrorContainer.get(i).getError();
                minKey = i;
            }
        }
        return minErrorContainer.get(minKey).getNeuronsNbr();
    }
}
